<template>
	<p class="text-center text-2xl pt-8 pb-4">{{ $t('miniGame.title') }}</p>
	<div class="game-con" id="gameContainer">
		<div id="container">
			<div id="game"></div>
			<div id="score">0</div>
			<div id="instructions">{{ $t('miniGame.guide') }}</div>
			<div class="game-over">
				<h2>{{ $t('miniGame.gameOver') }}</h2>
				<p>{{ $t('miniGame.hint1') }}</p>
				<p>{{ $t('miniGame.hint2') }}</p>
			</div>
			<div class="game-ready">
				<div id="start-button" class="cursor-pointer">{{ $t('miniGame.start') }}</div>
				<div></div>
			</div>
		</div>
	</div>
</template>

<script setup lang="ts">
import { nextTick } from 'vue'

import * as THREE from 'three'
import { Power1, TweenLite } from 'gsap'

nextTick(() => {
	// console.clear()

	interface BlockReturn {
		placed?: any
		chopped?: any
		plane: 'x' | 'y' | 'z'
		direction: number
		bonus?: boolean
	}

	class Stage {
		private container: any
		private camera: any
		private scene: any
		private renderer: any
		private light: any
		private softLight: any
		private group: any

		constructor() {
			// container

			this.container = document.getElementById('game')

			// renderer

			this.renderer = new THREE.WebGLRenderer({
				antialias: true,
				alpha: false,
			})

			this.renderer.setSize(window.innerWidth, window.innerHeight)
			this.renderer.setClearColor('#D0CBC7', 1)
			this.container.appendChild(this.renderer.domElement)

			// scene

			this.scene = new THREE.Scene()

			// camera

			let aspect = window.innerWidth / window.innerHeight
			let d = 20
			this.camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, -100, 1000)
			this.camera.position.x = 2
			this.camera.position.y = 2
			this.camera.position.z = 2
			this.camera.lookAt(new THREE.Vector3(0, 0, 0))

			//light

			this.light = new THREE.DirectionalLight(0xffffff, 0.5)
			this.light.position.set(0, 499, 0)
			this.scene.add(this.light)

			this.softLight = new THREE.AmbientLight(0xffffff, 0.4)
			this.scene.add(this.softLight)

			window.addEventListener('resize', () => this.onResize())
			this.onResize()
		}

		setCamera(y: number, speed: number = 0.3) {
			TweenLite.to(this.camera.position, speed, { y: y + 4, ease: Power1.easeInOut })
			TweenLite.to(this.camera.lookAt, speed, { y: y, ease: Power1.easeInOut })
		}

		onResize() {
			let viewSize = 30
			this.renderer.setSize(window.innerWidth, window.innerHeight)
			this.camera.left = window.innerWidth / -viewSize
			this.camera.right = window.innerWidth / viewSize
			this.camera.top = window.innerHeight / viewSize
			this.camera.bottom = window.innerHeight / -viewSize
			this.camera.updateProjectionMatrix()
		}

		render = function () {
			this.renderer.render(this.scene, this.camera)
		}

		add = function (elem) {
			this.scene.add(elem)
		}

		remove = function (elem) {
			this.scene.remove(elem)
		}
	}

	class Block {
		STATES = { ACTIVE: 'active', STOPPED: 'stopped', MISSED: 'missed' }
		MOVE_AMOUNT = 12

		dimension = { width: 0, height: 0, depth: 0 }
		position = { x: 0, y: 0, z: 0 }

		mesh: any
		state: string
		index: number
		speed: number
		direction: number
		colorOffset: number
		color: number
		material: any

		workingPlane: string
		workingDimension: string

		targetBlock: Block

		constructor(block: Block) {
			// set size and position

			this.targetBlock = block

			this.index = (this.targetBlock ? this.targetBlock.index : 0) + 1
			this.workingPlane = this.index % 2 ? 'x' : 'z'
			this.workingDimension = this.index % 2 ? 'width' : 'depth'

			// set the dimensions from the target block, or defaults.

			this.dimension.width = this.targetBlock ? this.targetBlock.dimension.width : 10
			this.dimension.height = this.targetBlock ? this.targetBlock.dimension.height : 2
			this.dimension.depth = this.targetBlock ? this.targetBlock.dimension.depth : 10

			this.position.x = this.targetBlock ? this.targetBlock.position.x : 0
			this.position.y = this.dimension.height * this.index
			this.position.z = this.targetBlock ? this.targetBlock.position.z : 0

			this.colorOffset = this.targetBlock ? this.targetBlock.colorOffset : Math.round(Math.random() * 100)

			// set color
			if (!this.targetBlock) {
				this.color = 0x333344
			} else {
				let offset = this.index + this.colorOffset
				var r = Math.sin(0.3 * offset) * 55 + 200
				var g = Math.sin(0.3 * offset + 2) * 55 + 200
				var b = Math.sin(0.3 * offset + 4) * 55 + 200
				this.color = new THREE.Color(r / 255, g / 255, b / 255)
			}

			// state

			this.state = this.index > 1 ? this.STATES.ACTIVE : this.STATES.STOPPED

			// set direction

			this.speed = -0.1 - this.index * 0.005
			if (this.speed < -4) this.speed = -4
			this.direction = this.speed

			// create block

			let geometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth)
			geometry.applyMatrix(
				new THREE.Matrix4().makeTranslation(
					this.dimension.width / 2,
					this.dimension.height / 2,
					this.dimension.depth / 2
				)
			)
			this.material = new THREE.MeshToonMaterial({ color: this.color, shading: THREE.FlatShading })
			this.mesh = new THREE.Mesh(geometry, this.material)
			this.mesh.position.set(
				this.position.x,
				this.position.y + (this.state == this.STATES.ACTIVE ? 0 : 0),
				this.position.z
			)

			if (this.state == this.STATES.ACTIVE) {
				this.position[this.workingPlane] = Math.random() > 0.5 ? -this.MOVE_AMOUNT : this.MOVE_AMOUNT
			}
		}

		reverseDirection() {
			this.direction = this.direction > 0 ? this.speed : Math.abs(this.speed)
		}

		place(): BlockReturn {
			this.state = this.STATES.STOPPED

			let overlap =
				this.targetBlock.dimension[this.workingDimension] -
				Math.abs(this.position[this.workingPlane] - this.targetBlock.position[this.workingPlane])

			let blocksToReturn: BlockReturn = {
				plane: this.workingPlane,
				direction: this.direction,
			}

			if (this.dimension[this.workingDimension] - overlap < 0.3) {
				overlap = this.dimension[this.workingDimension]
				blocksToReturn.bonus = true
				this.position.x = this.targetBlock.position.x
				this.position.z = this.targetBlock.position.z
				this.dimension.width = this.targetBlock.dimension.width
				this.dimension.depth = this.targetBlock.dimension.depth
			}

			if (overlap > 0) {
				let choppedDimensions = {
					width: this.dimension.width,
					height: this.dimension.height,
					depth: this.dimension.depth,
				}
				choppedDimensions[this.workingDimension] -= overlap
				this.dimension[this.workingDimension] = overlap

				let placedGeometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth)
				placedGeometry.applyMatrix(
					new THREE.Matrix4().makeTranslation(
						this.dimension.width / 2,
						this.dimension.height / 2,
						this.dimension.depth / 2
					)
				)
				let placedMesh = new THREE.Mesh(placedGeometry, this.material)

				let choppedGeometry = new THREE.BoxGeometry(
					choppedDimensions.width,
					choppedDimensions.height,
					choppedDimensions.depth
				)
				choppedGeometry.applyMatrix(
					new THREE.Matrix4().makeTranslation(
						choppedDimensions.width / 2,
						choppedDimensions.height / 2,
						choppedDimensions.depth / 2
					)
				)
				let choppedMesh = new THREE.Mesh(choppedGeometry, this.material)

				let choppedPosition = {
					x: this.position.x,
					y: this.position.y,
					z: this.position.z,
				}

				if (this.position[this.workingPlane] < this.targetBlock.position[this.workingPlane]) {
					this.position[this.workingPlane] = this.targetBlock.position[this.workingPlane]
				} else {
					choppedPosition[this.workingPlane] += overlap
				}

				placedMesh.position.set(this.position.x, this.position.y, this.position.z)
				choppedMesh.position.set(choppedPosition.x, choppedPosition.y, choppedPosition.z)

				blocksToReturn.placed = placedMesh
				if (!blocksToReturn.bonus) blocksToReturn.chopped = choppedMesh
			} else {
				this.state = this.STATES.MISSED
			}

			this.dimension[this.workingDimension] = overlap

			return blocksToReturn
		}

		tick() {
			if (this.state == this.STATES.ACTIVE) {
				let value = this.position[this.workingPlane]
				if (value > this.MOVE_AMOUNT || value < -this.MOVE_AMOUNT) this.reverseDirection()
				this.position[this.workingPlane] += this.direction
				this.mesh.position[this.workingPlane] = this.position[this.workingPlane]
			}
		}
	}

	class Game {
		STATES = {
			LOADING: 'loading',
			PLAYING: 'playing',
			READY: 'ready',
			ENDED: 'ended',
			RESETTING: 'resetting',
		}
		blocks: Block[] = []
		state: string = this.STATES.LOADING

		// groups

		newBlocks: any
		placedBlocks: any
		choppedBlocks: any

		// UI elements

		scoreContainer: any
		mainContainer: any
		startButton: any
		instructions: any

		constructor() {
			this.stage = new Stage()

			this.mainContainer = document.getElementById('container')
			this.scoreContainer = document.getElementById('score')
			this.startButton = document.getElementById('start-button')
			this.instructions = document.getElementById('instructions')
			this.scoreContainer.innerHTML = '0'

			this.newBlocks = new THREE.Group()
			this.placedBlocks = new THREE.Group()
			this.choppedBlocks = new THREE.Group()

			this.stage.add(this.newBlocks)
			this.stage.add(this.placedBlocks)
			this.stage.add(this.choppedBlocks)

			this.addBlock()
			this.tick()

			this.updateState(this.STATES.READY)

			// document.addEventListener('keydown', (e) => {
			// 	if (e.keyCode == 32) this.onAction()
			// })

			document.getElementById('gameContainer').addEventListener('click', (e) => {
				this.onAction()
			})

			document.getElementById('gameContainer').addEventListener('touchend', (e) => {
				e.preventDefault()
				this.onAction()

				// ☝️ this triggers after click on android so you
				// insta-lose, will figure it out later.
			})
		}

		updateState(newState) {
			for (let key in this.STATES) this.mainContainer.classList.remove(this.STATES[key])
			this.mainContainer.classList.add(newState)
			this.state = newState
		}

		onAction() {
			switch (this.state) {
				case this.STATES.READY:
					this.startGame()
					break
				case this.STATES.PLAYING:
					this.placeBlock()
					break
				case this.STATES.ENDED:
					this.restartGame()
					break
			}
		}

		startGame() {
			if (this.state != this.STATES.PLAYING) {
				this.scoreContainer.innerHTML = '0'
				this.updateState(this.STATES.PLAYING)
				this.addBlock()
			}
		}

		restartGame() {
			this.updateState(this.STATES.RESETTING)

			let oldBlocks = this.placedBlocks.children
			let removeSpeed = 0.2
			let delayAmount = 0.02
			for (let i = 0; i < oldBlocks.length; i++) {
				TweenLite.to(oldBlocks[i].scale, removeSpeed, {
					x: 0,
					y: 0,
					z: 0,
					delay: (oldBlocks.length - i) * delayAmount,
					ease: Power1.easeIn,
					onComplete: () => this.placedBlocks.remove(oldBlocks[i]),
				})
				TweenLite.to(oldBlocks[i].rotation, removeSpeed, {
					y: 0.5,
					delay: (oldBlocks.length - i) * delayAmount,
					ease: Power1.easeIn,
				})
			}
			let cameraMoveSpeed = removeSpeed * 2 + oldBlocks.length * delayAmount
			this.stage.setCamera(2, cameraMoveSpeed)

			let countdown = { value: this.blocks.length - 1 }
			TweenLite.to(countdown, cameraMoveSpeed, {
				value: 0,
				onUpdate: () => {
					this.scoreContainer.innerHTML = String(Math.round(countdown.value))
				},
			})

			this.blocks = this.blocks.slice(0, 1)

			setTimeout(() => {
				this.startGame()
			}, cameraMoveSpeed * 1000)
		}

		placeBlock() {
			let currentBlock = this.blocks[this.blocks.length - 1]
			let newBlocks: BlockReturn = currentBlock.place()
			this.newBlocks.remove(currentBlock.mesh)
			if (newBlocks.placed) this.placedBlocks.add(newBlocks.placed)
			if (newBlocks.chopped) {
				this.choppedBlocks.add(newBlocks.chopped)
				let positionParams = {
					y: '-=30',
					ease: Power1.easeIn,
					onComplete: () => this.choppedBlocks.remove(newBlocks.chopped),
				}
				let rotateRandomness = 10
				let rotationParams = {
					delay: 0.05,
					x: newBlocks.plane == 'z' ? Math.random() * rotateRandomness - rotateRandomness / 2 : 0.1,
					z: newBlocks.plane == 'x' ? Math.random() * rotateRandomness - rotateRandomness / 2 : 0.1,
					y: Math.random() * 0.1,
				}
				if (newBlocks.chopped.position[newBlocks.plane] > newBlocks.placed.position[newBlocks.plane]) {
					positionParams[newBlocks.plane] = '+=' + 40 * Math.abs(newBlocks.direction)
				} else {
					positionParams[newBlocks.plane] = '-=' + 40 * Math.abs(newBlocks.direction)
				}
				TweenLite.to(newBlocks.chopped.position, 1, positionParams)
				TweenLite.to(newBlocks.chopped.rotation, 1, rotationParams)
			}

			this.addBlock()
		}

		addBlock() {
			let lastBlock = this.blocks[this.blocks.length - 1]

			if (lastBlock && lastBlock.state == lastBlock.STATES.MISSED) {
				return this.endGame()
			}

			this.scoreContainer.innerHTML = String(this.blocks.length - 1)

			let newKidOnTheBlock = new Block(lastBlock)
			this.newBlocks.add(newKidOnTheBlock.mesh)
			this.blocks.push(newKidOnTheBlock)

			this.stage.setCamera(this.blocks.length * 2)

			if (this.blocks.length >= 5) this.instructions.classList.add('hide')
		}

		endGame() {
			this.updateState(this.STATES.ENDED)
		}

		tick() {
			this.blocks[this.blocks.length - 1].tick()
			this.stage.render()
			requestAnimationFrame(() => {
				this.tick()
			})
		}
	}

	let game = new Game()
})
</script>

<style scoped lang="less">
.game-con {
	margin: 0;
	overflow: hidden;
	height: 80vh;
	width: 100%;
	position: relative;
}

@media screen and (min-width: 768px) {
	.game-con {
		height: 100vh;
	}
}

#container {
	width: 100%;
	height: 100%;

	#score {
		position: absolute;
		top: 20px;
		width: 100%;
		text-align: center;
		font-size: 10vh;
		transition: transform 0.5s ease;
		color: #333344;
		transform: translatey(-200px) scale(1);
	}

	#game {
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
	}

	.game-over {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 85%;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;

		* {
			transition: opacity 0.5s ease, transform 0.5s ease;
			opacity: 0;
			transform: translatey(-50px);
			color: #333344;
		}

		h2 {
			margin: 0;
			padding: 0;
			font-size: 40px;
		}
	}

	.game-ready {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: space-around;

		#start-button {
			transition: opacity 0.5s ease, transform 0.5s ease;
			opacity: 0;
			transform: translatey(-50px);

			border: 3px solid #333344;
			padding: 10px 20px;
			background-color: transparent;
			color: #333344;
			font-size: 30px;
		}
	}

	#instructions {
		position: absolute;
		width: 100%;
		top: 16vh;
		left: 0;
		text-align: center;
		transition: opacity 0.5s ease, transform 0.5s ease;

		opacity: 0;

		&.hide {
			opacity: 0 !important;
		}
	}

	&.playing,
	&.resetting {
		#score {
			transform: translatey(0px) scale(1);
		}
	}

	&.playing {
		#instructions {
			opacity: 1;
		}
	}

	&.ready {
		.game-ready {
			#start-button {
				opacity: 1;
				transform: translatey(0);
			}
		}
	}

	&.ended {
		#score {
			transform: translatey(6vh) scale(1.5);
		}

		.game-over {
			* {
				opacity: 1;
				transform: translatey(0);
			}

			p {
				transition-delay: 0.3s;
			}
		}
	}
}
</style>
